<script lang="jsx">
/**
 * 动态表单，根据 model + configs => 表单
 * @author zeng/704729872@qq.com
 * @date 2021/9/30 12:43
*/
import { defineComponent } from "vue";
import {
  Form,
  FormItem,
  Input,
  Select,
  SelectOption,
  DatePicker,
  Switch,
  Checkbox,
  CheckboxGroup,
  RadioGroup,
  Radio,
  Col, Row
} from 'ant-design-vue';

// 允许动态添加的局部组件
export const ComponentMap = new Map()
ComponentMap.set('Input', Input)
ComponentMap.set('Select', Select)
ComponentMap.set('DatePicker', DatePicker)
ComponentMap.set('Switch', Switch)
ComponentMap.set('Checkbox', Checkbox)
ComponentMap.set('CheckboxGroup', CheckboxGroup)
ComponentMap.set('Radio', Radio)
ComponentMap.set('RadioGroup', RadioGroup)

/**
 * 局部组件内的子组件数组
 * @param {Object} config
 * @param {number | string} index
 * @return {unknown[]|*[]}
 */
function getConfigChildren (config, index) {
  let { options, dom } = config;
  if (!options) return [];
  if (typeof index === 'number') {
    options = options[index]
  }
  switch (dom) {
    case 'Select': {
      return (options || []).map(item => (
          <SelectOption {...{ props: item }} />
      ));
    }
    case 'RadioGroup': {
      return (options || []).map(item => (
          <Radio label={item.value}>{item.label}</Radio>
      ));
    }
    case 'CheckboxGroup': {
      return (options || []).map(item => (
          <CheckboxGroup label={item.value}>{item.label}</CheckboxGroup>
      ));
    }
    default: {
      return [];
    }
  }
}

/**
 * 将数组转成根据 prop 为 key 的对象
 * @param {Array} array 需要转换的数组，使用 children 作为子集，children 必须是一个数组
 * @param {Object} obj 用来存储转换后的对象
 * @param {string[]} keyPaths key 的层级，假设：{ prop: 'zeng', children: [{ prop: 'sex' }] } -> zeng.sex
 * @param {number} deep 当前递归的层级
 * @return {{}}
 */
function arrayToObject (array, { obj = {}, keyPaths = [], deep = 0 } = {}) {
  for (const item of array) {
    keyPaths[deep] = item.prop
    obj[keyPaths.join('.')] = item;
    const children = item.children
    if (Array.isArray(children) && children.length) {
      arrayToObject(children, { obj, keyPaths, deep: deep + 1 });
    }
  }
  return obj
}

/**
 * 根据 dom 类型，获取 value 字段
 * @param dom
 */
function getValueFieldByDom (dom) {
  switch (dom) {
    case 'Switch': {
      return 'checked'
    }
    default: {
      return 'value'
    }
  }
}

/**
 * 返回对象的类型
 * @param {Array | Object | Map | Set | string | number | boolean | *} obj
 * @return {string} [object String] -> String
 */
function getObjType (obj) {
  return Object.prototype.toString.call(obj).slice(8, -1)
}

/**
 prop: 'name', // 字段
 slot: 'name', // 插槽
 label: '名称', // 表单项名称
 dom: 'Input', // 标签名，如：Input | Select，开头字母大写
 domProps: { // 标签属性，参考 https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1
      },
 formItemProps: { // 表单项属性，同标签属性
      },
 options: [], // Select | RadioGroup 的选项，{ label: string, value: string | number }[]
 required: true, // 是否是必须的填写的
 如：[{ prop: 'name', dom: 'el-input', value: 'zeng' }]
 * @author zeng/704729872@qq.com
 * @date 2021/9/5 21:34
 * @update 2021/9/28 22:17 之前是以[配置]渲染图表，现在改为以[模型]渲染
 */
/* https://github.com/vuejs/jsx-next/blob/dev/packages/babel-plugin-jsx/README-zh_CN.md */
export default defineComponent({
  name: "zeng-form",
  emits: ['change'],
  props: {
    configs: {
      type: Array,
      default: () => []
    },
    model: {
      type: Object,
      default: () => ({})
    },
    rowProps: {
      type: Object,
      default: () => ({})
    }
  },
  setup (props, { slots, emit }) {

    const toConfig = arrayToObject(props.configs)

    // 防抖
    function onChange (e, key, opt) {
      const isCheck = ['Switch', 'Checkbox'].includes(opt.config.dom)
      const target = e ? e.target : null;
      const value = target ? (isCheck ? target.checked : target.value) : e;
      emit('change', value, key, opt);
    }

    function getFormItemChildren ({ model = props.model, childrens = [], deep = 0, keyPaths = [] } = {}) {
      for (const key in model) {
        const value = model[key]
        keyPaths[deep] = key;
        const allKeyPath = keyPaths.join('.');
        const strKeyPath = allKeyPath.replace(/\.\d/g, '')
        const config = toConfig[strKeyPath]
        if (!config) continue;
        const index = keyPaths.slice(-2)[0]; // 如果是索引的话，这里将拿到索引
        const slotKey = keyPaths.join('-');
        // 处理复杂类型
        if (config.children?.length) {
          childrens.push(slots[slotKey + '-before']?.())
          let deepChildren = []
          switch (getObjType(value)) {
            case 'Array': {
              deepChildren = value.map((item, index) => getFormItemChildren({
                model: item,
                deep: deep + 2,
                keyPaths: [...keyPaths, index] // 完整的 key 取值路径
              }))
              break
            }
            default: {
              deepChildren = getFormItemChildren({ model: value, deep: deep + 1, keyPaths });
            }
          }
          // 这是给父级划分区域，放开可以看效果
          // childrens.push(<div data-prop={slotKey}>{deepChildren}</div>, slots[slotKey + '-after']?.())
          childrens.push(...deepChildren, slots[slotKey + '-after']?.())
          continue;
        }
        const dom = config.dom;
        const Comp = ComponentMap.get(dom) || dom
        const formItemProps = Object.assign({}, config.formItemProps, {
          label: config.label,
          name: keyPaths.join('.')
        })
        const compProps = {
          onChange: e => onChange(e, key, { config, model, allKeyPath }),
          [getValueFieldByDom(dom)]: value ?? config.defaultValue,
          ...config.domProps
        }
        const slotName = config.slot || slotKey
        childrens.push(
            <Col {...config.columnProps}>
              <FormItem
                  class={'zeng-form-item deep--' + deep}
                  {...formItemProps}
                  data-prop={strKeyPath}
                  key={config.prop}
              >
                {slots[slotName + '-before']?.({ config, deep, index })}
                {
                  slots[slotName]?.({ config, deep, index }) ?? (
                      <Comp {...compProps}>
                        {getConfigChildren(config, index)}
                      </Comp>
                  )
                }
                {slots[slotName + '-after']?.({ config, deep, index })}
              </FormItem>
            </Col>
        );
      }
      return childrens;
    }

    return () => (
        <Form class="zeng-form" model={props.model}>
          <Row {...props.rowProps}>{getFormItemChildren()}</Row>
        </Form>
    )
  }
})
</script>
